package xj.graph2d.shapes;

import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import xj.graph2d.DrawAttr;
import xj.graph2d.DrawShape;
import xj.graph2d.GraphEdgeBase;
import xj.graph2d.GraphNodeBase;
import xj.graph2d.OpenShape;
import xj.graph2d.util.DrawingUtilities;
import xj.lang.CodeWriter;
import xj.util.gui.Point2DFloat;

public class PolyLineShape 
  extends GraphEdgeBase 
  implements PolyShape, OpenShape {

  public PolyLineShape() {
    this(false);
    allowRotation = false;
  }

  public PolyLineShape(boolean createComp) {
    super(createComp);
    allowRotation = false;
    adapter = new PolyShapeAdapter(this, false);
  }

  public PolyLineShape(float x1, float y1) {
    this(x1, y1, false);
    allowRotation = false;
  }

  public PolyLineShape(float x1, float y1, boolean createComp) {
    this(createComp);
    allowRotation = false;
    // setEnds(x1, y1, x1, y1);
    setStart(x1, y1);
    super.setEnds(x1, y1, x1, y1);
  }

  @Override
  public DrawShape cloneDrawShape(boolean deepClone) 
    throws CloneNotSupportedException {
    // PolyLineShape s = new PolyLineShape(comp != null);
    PolyLineShape s = (PolyLineShape) makeInstance();
    copyTo(s);
    cloneSubshapes(s, deepClone);
    cloneModel(s);
    copyPointsTo(s);
    return s;
  }

  public DrawShape makeInstance() {
    return new PolyLineShape(comp != null);
  }

  @Override
  public String getName() {
    return "Polyline";
  }

  @Override
  public boolean isEdgeSensitive() {
    return false;
  }

  @Override
  public boolean isCornorSensitive() {
    return false;
  }

  public void scaleFirstLast(float x1, float y1, float x2, float y2) {
    scaleFirstLast(x1, y1, x2, y2, false);
  }

  public void scaleFirstLast(float x1, float y1, float x2, float y2,
			     boolean limit) {
    adapter.scaleFirstLast(x1, y1, x2, y2, null, limit);
  }

  @Override
  public void drawOutline(Graphics2D g2, float x1, float y1, float x2, float y2) {
    DrawAttr attr = getDrawAttr();
    if (attr != null) {
      g2.setColor(attr.getPenColor());
    } else {
      g2.setColor(DrawAttr.getDefaultPenColor());
    }
    g2.draw(new Line2D.Float(x1, y1, x2, y2));
    // setEndsAdj(x1, y1, x2, y2);
    // draw(g2);
  }

  @Override
  public void draw(Graphics2D g2) {
    if (head != null || tail != null) {
      setArrowPosition();
    }
    float x1, y1, x2, y2;
    int n = adapter.getNumberOfPoints() - 1;
    float x = adapter.getX();
    float y = adapter.getY();
    float prevx = x;
    float prevy = y;
    float curx, cury;
    GeneralPath polyline = new GeneralPath(Path2D.WIND_EVEN_ODD, n);
    if (n > 0) { 
      polyline.moveTo(x, y);
      for (int i = 0; i < n; i++) {
	Point2D p = adapter.getPoint(i);
	if (p != null) {
	  curx = (float) p.getX() + x;
	  cury = (float) p.getY() + y;
	  x1 = prevx;
	  y1 = prevy;
	  x2 = curx;
	  y2 = cury;
	  if (i == 0 && 
	      tail != null && 
	      tail.getWidth() < Point2D.distance(x1, y1, x2, y2)) {
	    tail.draw(g2);
	    Point2D p1 = tail.getTailPosition();
	    x1 = (float) p1.getX();
	    y1 = (float) p1.getY();
	    polyline.moveTo(x1, y1);
	  }
	  if (i == (n - 1) && 
	      head != null && 
	      head.getWidth() < Point2D.distance(x1, y1, x2, y2)) {
	    head.draw(g2);
	    Point2D p2 = head.getTailPosition();
	    x2 = (float) p2.getX();
	    y2 = (float) p2.getY();
	  }
	  polyline.lineTo(x2, y2);
	  prevx = curx;
	  prevy = cury;
	}
      }
      g2.draw(polyline);
    }
  }

  @Override
  public Shape getOutlineShape() {
    return makePolyLine();
  }

  protected GeneralPath makePolyLine() { 
    int n = adapter.getNumberOfPoints() - 1;
    float x = adapter.getX();
    float y = adapter.getY();
    float curx, cury;
    GeneralPath polyline = new GeneralPath(Path2D.WIND_EVEN_ODD, n);
    polyline.moveTo(x, y);
    for (int i = 0; i < n; i++) {
      Point2D p = adapter.getPoint(i);
      if (p != null) {
	curx = (float) p.getX() + x;
	cury = (float) p.getY() + y;
	polyline.lineTo(curx, cury);
      }
    }
    return polyline;
  }

  @Override
  public void setArrowPosition() {
    int n = adapter.getNumberOfPoints() - 1;
    if (n >= 1) {
      float x1 = adapter.getX();
      float y1 = adapter.getY();
      Point2D p = adapter.getPoint(0);
      float cx1 = (float) p.getX() + x1;
      float cy1 = (float) p.getY() + y1;
      p = adapter.getPoint(n - 1);
      float x2 = (float) p.getX() + x1;
      float y2 = (float) p.getY() + y1;
      float cx2 = x1;
      float cy2 = y1;
      if (n > 1) {
	p = adapter.getPoint(n - 2);
	cx2 = (float) p.getX() + x1;
	cy2 = (float) p.getY() + y1;
      }
      if (head != null) {
	head.setPosition(x2, y2);
	head.setAngle((float) DrawingUtilities.calculateRotationAngle(x2, y2, cx2, cy2));
      }
      if (tail != null) {
	tail.setPosition(x1, y1);
	tail.setAngle((float) DrawingUtilities.calculateRotationAngle(x1, y1, cx1, cy1));
      }
    }
  }

  public void setFirstLast(float x1, float y1, float x2, float y2) {

    if (debug) {
      System.out.println("PolyLineShape.setFirstLast() before n=" + 
			 adapter.getNumberOfPoints());
    }

    // adapter.setStart(x1, y1);
    // adapter.setFirst(x1, y1);
    float x = getX();
    float y = getY();
    adapter.moveStartOnly(x1 - x, y1 - y);
    if (adapter.getNumberOfPoints() == 1) {
      adapter.addPoint(x2, y2, true);
    } else {
      adapter.setLast(x2, y2);
    }
    // setEnds(x1, y1, x2, y2);
    adapter.calculateBounds();

    if (debug) {
      System.out.println("PolyLineShape.setFirstLast() after  n=" + 
			 adapter.getNumberOfPoints());
    }
  }

  @Override
  public Point2D getEnd1() {
    return adapter.getAnchorPoint();
  }

  @Override
  public Point2D getEnd2() {
    return adapter.getLastPoint();
  }

  public void swapEnds() { 
    super.swapEnds();
    adapter.reversePoints();
  }

  @Override
  public void adjustEnds(GraphNodeBase src, 
			 GraphNodeBase des, 
			 boolean adjustCtrPts) {
    if (debug) {
      System.out.println("PolyLineShape.adjustEnds()");
    }

    Point2D q1 = null, q2 = null;
    Point2D r1, r2;
    float x = adapter.getX();
    float y = adapter.getY();
    int n = adapter.getNumberOfPoints();
    if (n > 2) {
      if (debug) {
	System.out.println("PolyLineShape.adjustEnds() n > 2");
      }

      Point2D p = adapter.getPoint(0);
      r1 = new Point2DFloat((float) p.getX() + x, (float) p.getY() + y);
      p = adapter.getPoint(n - 3);
      r2 = new Point2DFloat((float) p.getX() + x, (float) p.getY() + y);
    } else {
      r1 = adapter.getLastPoint();
      r2 = new Point2DFloat(x, y);
    }

    float x1, y1, x2, y2;
    if (src != null) {
      q1 = src.intersect(r1, 
			 tailOffset.dx * src.getShapeWidth(), 
			 tailOffset.dy * src.getShapeHeight());
    }
    if (q1 != null) {
      x1 = (float) q1.getX();
      y1 = (float) q1.getY();
      // setFirst(x1, y1);
    } else {
      x1 = x;
      y1 = y;
    }

    if (des != null) {
      q2 = des.intersect(r2, 
			 headOffset.dx * des.getShapeWidth(), 
			 headOffset.dy * des.getShapeHeight());
    }
    if (q2 != null) {
      x2 = (float) q2.getX();
      y2 = (float) q2.getY();
      // setLast(x2, y2);
    } else {
      Point2D last = adapter.getLastPoint();
      x2 = (float) last.getX();
      y2 = (float) last.getY();
    }

    // scaleFirstLast(x1, y1, x2, y2, true);
    if (src == des) { 
      adapter.setFirst(x1, y1);
      adapter.setLast(x2, y2);    
    } else { 
      if (adjustCtrPts) { 
	adapter.scaleFirstLast(x1, y1, x2, y2, true);
	adjustNextToEndPoints(src, des);
      } else { 
	adapter.setFirstOnly(x1, y1);
	adapter.setLast(x2, y2);
      }
    }

    adapter.invalidateBounds();
    calculateBounds();
  }

  protected void adjustNextToEndPoints(GraphNodeBase src, 
				       GraphNodeBase des) { 
    int n = adapter.getNumberOfPoints();
    if (n > 2) { 	
      float x = adapter.getX();
      float y = adapter.getY();
      Point2D p3 = adapter.getPoint(0);     // the second point 	
      Point2D p4 = adapter.getPoint(n - 3); // the second to the last point  

      if (src != null) { 
	float srcW = src.getShapeWidth() / 2;
	float srcH = src.getShapeHeight() / 2;
	Point2D srcCtr = src.getCenter();
	float cx1 = (float) srcCtr.getX();
	float cy1 = (float) srcCtr.getY();
	float px = (float) p3.getX() + x;
	float py = (float) p3.getY() + y;
	if (py < cy1 + srcH &&
	    py > cy1 - srcH && 
	    px < cx1 + srcW &&
	    px > cx1 - srcW) { 
	  if (py > cy1) { 
	    py = cy1 + srcH + nodeSep;
	  } else { 
	    py = cy1 - srcH - nodeSep;
	  }
	  if (px > cx1) { 
	    px = cx1 + srcW + nodeSep;
	  } else { 
	    px = cx1 - srcW - nodeSep;
	  }
	  adapter.setPoint(1, px, py);
	}
      }
      if (des != null) { 
	float desW = des.getShapeWidth() / 2;
	float desH = des.getShapeHeight() / 2;
	Point2D desCtr = des.getCenter();
	float cx2 = (float) desCtr.getX();
	float cy2 = (float) desCtr.getY();
	float px = (float) p4.getX() + x;
	float py = (float) p4.getY() + y;
	if (py < cy2 + desH &&
	    py > cy2 - desH && 
	    px < cx2 + desW &&
	    px > cx2 - desW) { 
	  if (py > cy2) { 
	    py = cy2 + desH + nodeSep;
	  } else { 
	    py = cy2 - desH - nodeSep;
	  }
	  if (px > cx2) { 
	    px = cx2 + desW + nodeSep;
	  } else { 
	    px = cx2 - desW - nodeSep;
	  }
	  adapter.setPoint(n - 2, px, py);
	}
      }      
    }
  }

  @Override
  public String toString() {
    return super.toString() + "\ncomp=" + comp + "\n" + adapter.toString();
  }

  @Override
  public String detailString() { 
    return super.detailString() + "\n  points=" + adapter.toString();
  }

  // PolyShapeAdapter

  /** (x, y) is the starting, i.e. anchor, point of the polyshape */
  public float getX() {
    return adapter.getX();
  }

  public float getY() {
    return adapter.getY();
  }

  public void setFirst(float x, float y) {
    adapter.setFirst(x, y);
  }

  public void setStart(float x, float y) {
    adapter.setStart(x, y);
  }

  /** add a new point at the end, coincide with the current last point */
  public void addPoint() {
    adapter.addPoint();
  }

  /** add a new point at the end, (x, y) is absolute */
  public void addPoint(float x, float y) {
    adapter.addPoint(x, y);
  }

  /**
   * add a new point at the end. adj == true: (x, y) is absolute, needs to
   * be adjusted to be relative to the starting point adj == false: (x, y)
   * is relative to the starting point
   */
  public void addPoint(float x, float y, boolean adj) {
    adapter.addPoint(x, y, adj);
  }

  public void moveStartOnly(float dx, float dy) {
    adapter.moveStartOnly(dx, dy);
  }

  /** move the i-th vertex by (dx, dy) */
  public void moveVertex(int i, float dx, float dy) {
    adapter.moveVertex(i, dx, dy);
  }

  /** move the i-th vertex to (x, y), absolute coord */
  public void setPoint(int i, float x, float y) {
    adapter.setPoint(i, x, y);
  }

  public Point2D getPoint(int i) {
    return adapter.getPoint(i);
  }

  /** add a new point at the i-th position, (x, y) is absolute */
  public void addPoint(int i, float x, float y) {
    adapter.addPoint(i, x, y);
  }

  /**
   * add a new point at the i-th position. adj == true: (x, y) is
   * absolute, needs to be adjusted to be relative to the starting point
   * adj == false: (x, y) is relative to the starting point
   */
  public void addPoint(int i, float x, float y, boolean adj) {
    adapter.addPoint(i, x, y, adj);
  }

  public void removePoint(int i) {
    adapter.removePoint(i);
  }

  public void setLast(float x, float y) {
    adapter.setLast(x, y);
  }

  public void drawPolyLine(Graphics2D g2) {
    adapter.drawPolyLine(g2);
  }

  public Point2D getLast() {
    return adapter.getLast();
  }

  public int getNumberOfPoints() {
    return adapter.getNumberOfPoints();
  }

  @Override
  public void move(float dx, float dy) {
    adapter.move(dx, dy);
    super.move(dx, dy);
  }

  @Override
  public void drawFrame(Graphics2D g2) {
    adapter.drawFrame(g2, true, comp != null);
  }

  @Override
  public void scale(float sx, float sy) {
    adapter.scale(sx, sy);
  }

  @Override
  public void setEndsAdj(float x1, float y1, float x2, float y2) {
    adapter.setEndsAdj(x1, y1, x2, y2);
  }

  @Override
  public float getShapeWidth() {
    return (float) bounds.getWidth();
  }

  @Override
  public float getShapeHeight() {
    return (float) bounds.getHeight();
  }

  @Override
  public int isInside(float px, float py) {
    return adapter.isInside(px, py);
  }

  @Override
  public void calculateBounds() {
    adapter.calculateBounds();
    super.calculateBounds();
  }

  @Override
  protected Map<String, Object> getXMLAttributes() {
    Map<String, Object> attrs = super.getXMLAttributes();
    if (attrs == null) {
      attrs = new HashMap<String, Object>();
    }
    attrs.put("x", adapter.getX());
    attrs.put("y", adapter.getY());
    return attrs;
  }

  @Override
  protected void writeXMLElements(CodeWriter out, int mode,
				  String namespacePrefix) {
    if (out != null) {
      super.writeXMLElements(out, mode, namespacePrefix);

      writeXMLPoints(out, adapter.getPoints(), namespacePrefix);
    }
  }

  @Override
  protected void processXMLAttributes(org.w3c.dom.Element node) {
    if (node != null) {
      super.processXMLAttributes(node);

      String xs, ys;
      float x, y;
      xs = node.getAttribute("x");
      ys = node.getAttribute("y");
      try {
	x = Float.parseFloat(xs);
	y = Float.parseFloat(ys);
	adapter.setFirst(x, y);
      } catch (NumberFormatException ex) {
      }
    }
  }

  @Override
  protected void processXMLElement(org.w3c.dom.Element e, String namespaceURI) {
    if (e != null) {
      if ("Points".equals(e.getLocalName())) {
	adapter.setAttrFromXML(e, namespaceURI);
      } else {
	super.processXMLElement(e, namespaceURI);
      }
    }
  }

  public List<Point2D> getPoints() {
    return adapter.getPoints();
  }

  public void copyPointsTo(PolyShape s) {
    adapter.copyPointsTo(s);
  }

  protected PolyShapeAdapter adapter;

  public static final float DEFAULT_NODE_SEP = 20;

  protected float nodeSep = DEFAULT_NODE_SEP; 

  protected static final boolean debug = false;

}
